﻿//---------------------------------------------------------------------
// <copyright file="NextLinkResponseVerifier.cs" company="Microsoft">
//      Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
// </copyright>
//---------------------------------------------------------------------

namespace Microsoft.Test.Taupo.Astoria.ResponseVerification
{
    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.Test.Taupo.Astoria.Common;
    using Microsoft.Test.Taupo.Astoria.Contracts;
    using Microsoft.Test.Taupo.Astoria.Contracts.EntityModel;
    using Microsoft.Test.Taupo.Astoria.Contracts.Http;
    using Microsoft.Test.Taupo.Astoria.Contracts.LinqToAstoria;
    using Microsoft.Test.Taupo.Astoria.Contracts.OData;
    using Microsoft.Test.Taupo.Astoria.Contracts.ResponseVerification;
    using Microsoft.Test.Taupo.Common;
    using Microsoft.Test.Taupo.Execution;
#if WINDOWS_PHONE
    using Microsoft.Test.Taupo.Platforms; // for Enum.HasFlag
#endif
    using Microsoft.Test.Taupo.Query.Contracts;

    /// <summary>
    /// Response verifier for next links generated by server-driven-paging
    /// </summary>
    public class NextLinkResponseVerifier : ResponseVerifierBase, ISelectiveResponseVerifier
    {
        /// <summary>
        /// Gets or sets the expected next link generator
        /// </summary>
        [InjectDependency(IsRequired = true)]
        public INextLinkExpectationGenerator ExpectedNextLinkGenerator { get; set; }

        /// <summary>
        /// Gets or sets the assertion handler
        /// </summary>
        [InjectDependency(IsRequired = true)]
        public StackBasedAssertionHandler AssertHandler { get; set; }

        /// <summary>
        /// Gets or sets the protocol implementation details.
        /// </summary>
        [InjectDependency(IsRequired = true)]
        public IProtocolImplementationDetails ProtocolImplementationDetails { get; set; }

        /// <summary>
        /// Gets or sets the URI evaluator.
        /// </summary>
        [InjectDependency(IsRequired = true)]
        public IODataUriEvaluator UriEvaluator { get; set; }

        /// <summary>
        /// Returns true if this is an update request
        /// </summary>
        /// <param name="request">The request being verified</param>
        /// <returns>Whether or not this verifier applies to the request</returns>
        public bool Applies(ODataRequest request)
        {
            if (request.Uri.IsEntity() || request.Uri.IsEntitySet() || request.Uri.IsEntityReferenceLink())
            {
                if (request.Uri.IsAction())
                {
                    return false;
                }

                if (request.Uri.IsServiceOperation())
                {
                    return true;
                }

                return request.Verb == HttpVerb.Get;
            }

            return false;
        }

        /// <summary>
        /// Returns true if for all entity or entity set response payloads
        /// </summary>
        /// <param name="response">The response</param>
        /// <returns>Whether or not this verifier applies to the response</returns>
        public bool Applies(ODataResponse response)
        {
            return response.RootElement != null && (
                response.RootElement.ElementType == ODataPayloadElementType.EntityInstance
                || response.RootElement.ElementType == ODataPayloadElementType.EntitySetInstance
                || response.RootElement.ElementType == ODataPayloadElementType.LinkCollection);
        }

        /// <summary>
        /// Verifies the update succeeded
        /// </summary>
        /// <param name="request">The request to verify</param>
        /// <param name="response">The response to verify</param>
        public override void Verify(ODataRequest request, ODataResponse response)
        {
            base.Verify(request, response);

            var contentType = response.GetHeaderValueIfExists(HttpHeaders.ContentType);
            var version = response.GetDataServiceVersion();
            var options = this.ProtocolImplementationDetails.GetExpectedPayloadOptions(contentType, version, request.Uri);

            new NextLinkValidatingVisitor(request.Uri, options, this).ValidateNextLinks(response.RootElement);
        }

        /// <summary>
        /// Visitor which validates next links in feeds by calculating the expected next link
        /// TODO: link collections
        /// </summary>
        private class NextLinkValidatingVisitor : ODataPayloadElementVisitorBase
        {
            private readonly Stack<QueryValue> queryValueStack = new Stack<QueryValue>();
            private readonly Stack<EntityInstance> entityStack = new Stack<EntityInstance>();
            private readonly Stack<NavigationPropertyInstance> navigationStack = new Stack<NavigationPropertyInstance>();
            private readonly Stack<XmlBaseAnnotation> xmlBaseStack = new Stack<XmlBaseAnnotation>();
            private readonly ODataUri requestUri;
            private readonly ODataPayloadOptions payloadOptions;
            private readonly NextLinkResponseVerifier parent;

            /// <summary>
            /// Initializes a new instance of the <see cref="NextLinkValidatingVisitor"/> class.
            /// </summary>
            /// <param name="requestUri">The request URI.</param>
            /// <param name="payloadOptions">The payload options.</param>
            /// <param name="parent">The parent verifier.</param>
            public NextLinkValidatingVisitor(ODataUri requestUri, ODataPayloadOptions payloadOptions, NextLinkResponseVerifier parent)
            {
                ExceptionUtilities.CheckArgumentNotNull(requestUri, "requestUri");
                ExceptionUtilities.CheckArgumentNotNull(parent, "parent");
                this.requestUri = requestUri;
                this.payloadOptions = payloadOptions;
                this.parent = parent;

                this.queryValueStack.Push(this.parent.UriEvaluator.Evaluate(requestUri, true, true));
            }

            /// <summary>
            /// Validates the next links.
            /// </summary>
            /// <param name="rootElement">The root element.</param>
            public void ValidateNextLinks(ODataPayloadElement rootElement)
            {
                this.Recurse(rootElement);
            }

            /// <summary>
            /// Visits the payload element
            /// </summary>
            /// <param name="payloadElement">The payload element to visit</param>
            public override void Visit(EntityInstance payloadElement)
            {
                ExceptionUtilities.CheckArgumentNotNull(payloadElement, "payloadElement");
                using (this.parent.AssertHandler.WithMessage("In entity instance with id '{0}'", payloadElement.Id))
                {
                    try
                    {
                        this.entityStack.Push(payloadElement);

                        base.Visit(payloadElement);
                    }
                    finally
                    {
                        this.entityStack.Pop();
                    }
                }
            }

            /// <summary>
            /// Visits the payload element
            /// </summary>
            /// <param name="payloadElement">The payload element to visit</param>
            public override void Visit(EntitySetInstance payloadElement)
            {
                ExceptionUtilities.CheckArgumentNotNull(payloadElement, "payloadElement");

                var entitySetAnnotation = payloadElement.Annotations.OfType<EntitySetAnnotation>().SingleOrDefault();
                ExceptionUtilities.CheckObjectNotNull(entitySetAnnotation, "Could not find entity-set annotation on payload element");

                bool expectNextLink = false;
                var expectedPageSize = entitySetAnnotation.EntitySet.GetEffectivePageSize();
                if (expectedPageSize.HasValue)
                {
                    // note that the check that the number of elements is less-than-or-equal-to page size happens below
                    expectNextLink = payloadElement.Count == expectedPageSize.Value;
                }

                string message;
                if (this.navigationStack.Count == 0)
                {
                    message = "In root feed";

                    // if the service can tell from the uri that no paging is needed, there will be no next link
                    if (expectNextLink && this.requestUri.Top.HasValue)
                    {
                        expectNextLink &= this.requestUri.Top.Value > expectedPageSize.Value;
                    }
                }
                else
                {
                    message = "In expanded feed";
                }

                using (this.parent.AssertHandler.WithMessage(message))
                {
                    if (expectedPageSize.HasValue)
                    {
                        // verify number of elements. This is also validated based on query expectations in another verifier,
                        // but its cheap and easy to cover here as well to cover our bases.
                        this.parent.AssertHandler.IsTrue(
                            payloadElement.Count <= expectedPageSize.Value,
                            "Number of elements ({0}) exceeds page size ({1}).",
                            payloadElement.Count,
                            expectedPageSize.Value);
                    }

                    if (!expectNextLink)
                    {
                        this.parent.AssertHandler.IsNull(payloadElement.NextLink, "Next link unexpectedly non-null");
                    }
                    else
                    {
                        this.parent.AssertHandler.IsNotNull(payloadElement.NextLink, "Next link unexpectedly null");

                        // only validate the next link if the protocol is implemented based on the expected conventions
                        if (this.payloadOptions.HasFlag(ODataPayloadOptions.UseConventionBasedLinks))
                        {
                            this.GenerateAndCompareNextLink(payloadElement, expectedPageSize.Value);
                        }
                    }

                    this.Recurse(payloadElement);
                }
            }

            /// <summary>
            /// Visits the payload element
            /// </summary>
            /// <param name="payloadElement">The payload element to visit</param>
            public override void Visit(NavigationPropertyInstance payloadElement)
            {
                ExceptionUtilities.CheckArgumentNotNull(payloadElement, "payloadElement");

                using (this.parent.AssertHandler.WithMessage("In navigation property '{0}'", payloadElement.Name))
                {
                    var currentValue = this.queryValueStack.Peek() as QueryStructuralValue;
                    ExceptionUtilities.CheckObjectNotNull(currentValue, "The current value was not structural");

                    if (currentValue.Type.Properties.Any(p => p.Name == payloadElement.Name))
                    {
                        try
                        {
                            this.navigationStack.Push(payloadElement);
                            this.queryValueStack.Push(currentValue.GetValue(payloadElement.Name));
                            base.Visit(payloadElement);
                        }
                        finally
                        {
                            this.navigationStack.Pop();
                            this.queryValueStack.Pop();
                        }
                    }
                }
            }

            /// <summary>
            /// Wrapper for recursively visiting the given element. Used with the callback property to make unit tests easier.
            /// </summary>
            /// <param name="element">The element to visit</param>
            protected override void Recurse(ODataPayloadElement element)
            {
                ExceptionUtilities.CheckArgumentNotNull(element, "element");
                var xmlBase = element.Annotations.OfType<XmlBaseAnnotation>().SingleOrDefault();
                if (xmlBase == null)
                {
                    base.Recurse(element);
                }
                else
                {
                    using (new DelegateBasedDisposable(() => this.xmlBaseStack.Pop()))
                    {
                        this.xmlBaseStack.Push(xmlBase);
                        base.Recurse(element);
                    }
                }
            }

            private void Recurse(EntitySetInstance payloadElement)
            {
                if (payloadElement.Count == 0)
                {
                    return;
                }

                var currentValue = this.queryValueStack.Peek() as QueryCollectionValue;
                ExceptionUtilities.CheckObjectNotNull(currentValue, "Current value was not a collection");

                ExceptionUtilities.Assert(payloadElement.Count == currentValue.Elements.Count, "Number of elements did not match");

                for (int i = 0; i < payloadElement.Count; i++)
                {
                    try
                    {
                        this.queryValueStack.Push(currentValue.Elements[i]);
                        this.Recurse(payloadElement[i]);
                    }
                    finally
                    {
                        this.queryValueStack.Pop();
                    }
                }
            }

            private void GenerateAndCompareNextLink(EntitySetInstance payloadElement, int expectedPageSize)
            {
                var currentPageValues = this.queryValueStack.Peek() as QueryCollectionValue;
                ExceptionUtilities.CheckObjectNotNull(currentPageValues, "Current value was not a collection");
                var lastEntityValue = currentPageValues.Elements.Cast<QueryStructuralValue>().Last();

                // build the expected next link and the message to used based on whether we are in an expanded feed or top-level feed
                string expectedNextLink = null;
                if (this.navigationStack.Count == 0)
                {
                    expectedNextLink = this.parent.ExpectedNextLinkGenerator.GenerateNextLink(this.requestUri, expectedPageSize, lastEntityValue);
                }
                else
                {
                    expectedNextLink = this.parent.ExpectedNextLinkGenerator.GenerateExpandedNextLink(this.entityStack.Peek(), this.navigationStack.Peek(), lastEntityValue);
                }

                var xmlBaseSegments = this.xmlBaseStack.Reverse().Select(x => x.Value);
                var expected = UriHelpers.CreateAbsoluteLink(expectedNextLink, xmlBaseSegments);

                this.parent.AssertHandler.AreEqual(expected.OriginalString, payloadElement.NextLink, "Next link did not match expectation");
            }
        }
    }
}